package com.flyway.execise.flyway_db.recompile;

/*
 * Copyright (c) 2019, Kilimall and/or its affiliates. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   - Neither the name of Oracle nor the names of its
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 */

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.web.context.support.StandardServletEnvironment;

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;

import static com.flyway.execise.flyway_db.cart.CommonConstants.*;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKinds.*;
import static java.util.Locale.US;

/**
 * Example to watch a directory (or tree) for changes to files.
 *
 * @date 19-7-15
 * @auther jackliang
 * @description TODO
 */
@Slf4j
public final class RecompileNotifyWatcher {

    private WatchService watcher;
    private Map<WatchKey, Path> keys;
    private boolean recursive;
    private boolean trace;
    private Environment environment;
    private List<String> pPatternContainer = Arrays.asList(".properties");


    public RecompileNotifyWatcher(boolean recursive, Environment environment) {
        try {
            this.watcher = FileSystems.getDefault().newWatchService();
            this.keys = new HashMap<>(8);
            this.recursive = recursive;
            this.environment = environment;
        } catch (Exception e) {
            log.error("Watcher initialized error !", e);
            System.exit(-1);
        }

    }

    public  RecompileNotifyWatcher (){

    }

    /**
     * Creates a WatchService and registers the given directory
     */
    public RecompileNotifyWatcher(Path dir, boolean recursive, Environment environment) {
        this(recursive, environment);
        this.initRegisterAboutRecusive(dir, recursive);
        this.processEvents();
    }



    /**
     * Register the given directory with the WatchService
     */
    private void register(Path dir) throws IOException {
        WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
        if (trace) {
            Path prev = keys.get(key);
            if (prev == null) {
                log.debug("Path of directory register is {}\n", dir);
            } else {
                if (!dir.equals(prev)) {
                    log.debug("update directory register: %s -> {}\n", prev, dir);
                }
            }
        }
        keys.put(key, dir);
    }


    /**
     * Register the given directory, and all its sub-directories, with the
     * WatchService.
     */
    private void registerAll(final Path start) throws IOException {
        // register directory and sub-directories
        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                    throws IOException {
                register(dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }


    private void initRegisterAboutRecusive(Path dir, boolean recursive) {
        try {
            if (recursive) {
                log.info("Scanning register directory for {} ...\n", dir);
                this.registerAll(dir);
                log.info("Register Done.");
            } else {
                register(dir);
            }
        } catch (IOException e) {
            log.error("Running construct method matches error~");
            System.exit(-1);
        }

        /* enable trace after initial registration */
        this.trace = true;
    }




    <T> WatchEvent<T> cast(WatchEvent<?> event) {
        return (WatchEvent<T>) event;
    }


    /**
     * Process all events for keys queued to the watcher
     */
    public void processEvents() {

        level:
         while (true) {
            /* wait for key to be signalled */
            WatchKey key;
            try {
                key = watcher.take();
                log.debug("Hold on that for starting signal key {}", key.toString());
            } catch (InterruptedException x) {
                return;
            }
            //key.
            Path dir = keys.get(key);
            if (dir == null) {
                log.info("WatchKey haven't been recognized!!");
                continue;
            }

            /* StartUp to  heat deploy logically ~ */
            try {

                /* Delete key event to be handled */
                if (!this.catchDeleteEvent(key)) {

                    for (WatchEvent<?> event : key.pollEvents()) {
                        WatchEvent.Kind kind = event.kind();
                        log.debug("Current event be catched ,which is {} type", kind.type());

                        /* TBD - provide example of how OVERFLOW event is handled */
                        if (kind == OVERFLOW) {
                            continue;
                        }

                        //TODO  Need to refine a major event in many incidents
                        Iterator<String> iterator = pPatternContainer.iterator();
                        boolean flag =  false;

                        /* Context for directory entry event is the file name of entry */
                        Path child = this.handleInvocableEventPath(dir, event);

                        while (iterator.hasNext()){
                            if (child.toString().endsWith(iterator.next())){
                                flag = true;
                                break;
                            }
                        }

                        if (!flag){
                            continue level;
                        }

                        /* Path to the event about handling for validation */
                        if (this.formalPathValidate(child)) {
                            /* property hot reload */
                            this.PropertyReloadThroughPath(child);

                            /* if directory is created, and watching recursively, then register it and its sub-directories */
                            if (recursive && (kind == ENTRY_CREATE)) {
                                try {
                                    if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
                                        registerAll(child);
                                    }
                                } catch (IOException x) {
                                    /* ignore to keep sample readable */
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                log.error("Internal errors occur", e);
                e.fillInStackTrace();
                continue;
                /* Ignore error , but only to print error stack message */
            }

            /* reset key and remove from set if directory no longer accessible */
            boolean valid = key.reset();
            if (!valid) {
                keys.remove(key);

                /* all directories are inaccessible */
                if (keys.isEmpty()) {
                    break;
                }
            }
        }
    }


    private boolean formalPathValidate(Path dir) {
        return dir.toString().endsWith(SUBFFIX_PROPERTIES) ||
                dir.toString().endsWith(SUBFFIX_JAVA);
    }


    private boolean catchDeleteEvent(WatchKey key) throws Exception {
        Iterator<WatchEvent<?>> iter = key.pollEvents().iterator();

        if (iter.hasNext() && iter.next().kind() == ENTRY_DELETE) {
            log.info("Delete entry signal was be catched for {}", ENTRY_DELETE.name());
            this.doCompile();
            return true;
        }
        return false;
    }


    /* Path analysis */
    private Path handleInvocableEventPath(Path dir, WatchEvent<?> event) {
        WatchEvent<Path> ev = this.cast(event);
        Path name = ev.context();
        return dir.resolve(name);
    }


    /**
     * Load the spring cloud config configuration and integrate the properties of the hot deployment
     *
     * @param path
     * @throws Exception
     */
    protected void PropertyReloadThroughPath(Path path) {
        assert environment instanceof StandardServletEnvironment;
        StandardServletEnvironment standardServletEnvironment = (StandardServletEnvironment) environment;

        synchronized (standardServletEnvironment) {
            MutablePropertySources mutablePropertySources = standardServletEnvironment.getPropertySources();
            PropertySource propertySource = mutablePropertySources.get(MULTABLE_PROPERTY_VALUE);
            Map<String, Object> maps = ((OriginTrackedMapPropertySource) propertySource).getSource();

            if (!path.toString().endsWith(SUBFFIX_PROPERTIES)) {
                return;
            }

            try (InputStream uls = path.
                    toUri().
                    toURL().
                    openStream()) {

                Properties p = new Properties();
                p.load(uls);

                log.info("Extra value map has been added into environment,newly loaded value is {}", p.toString());

                assert p instanceof Map;

                maps.putAll((Map) p);
            } catch (Exception e) {
                /* If the path is just moving, it is possible to detect an abnormal flow. just ignore*/
                return;
            }
        }

    }

    /**
     * pre-compile-handle
     *
     * @throws IOException
     * @throws InterruptedException
     */
    private void doCompile() throws Exception {
        //SystemUtils.IS_OS_LINUX

        if (!this.isLinux()){return;}

        Process process = Runtime.getRuntime().exec(DEPLOY_STATIC_ACTION);
        int status = process.waitFor();

        if (status != 0) {
            log.error("Failed to call shell's command");
        }

        try (BufferedReader var = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            while (var.readLine() != null) {
                log.info(var.readLine());
            }
        }
    }

    private boolean isLinux() {
        return System.getProperty(OS_NAME).toUpperCase(US).equalsIgnoreCase(LINUX);
    }


    private void init() {
        Path dir = Paths.get(System.getProperty(USER_DIR) + File.separator + "src");
        new Thread(()-> {
            try {
                new RecompileNotifyWatcher(dir, true,environment);
            } catch (Exception e) {
               // ExceptionUtils.printRootCauseStackTrace(e);
            }
        }).start();
    }
}
